class_name Utility2D extends Object
## 工具类


# 空字符串
const STRING_EMPTY: String = ""
# 类与脚本名的格式字符串
const CLASS_AND_SCRIPT_NAME_FORMAT: StringName = "class:{0}, script:{1}"
# 属性名-global_position
const PROPERTY_NAME_GLOBAL_POSITION: StringName = "global_position"
# 方法名-get_global_rect
const METHOD_NAME_GET_GLOBAL_RECT: StringName = "get_global_rect"
# 获取位置失败时，返回的位置
const POSITION_FAILED: Vector2 = Vector2.INF
# 获取边界框失败时，返回的边界框
const RECT_FAILED: Rect2 = Rect2(Vector2.INF, Vector2.ZERO)


#region common
# 缓存类的属性列表，键为类与脚本名，值为属性列表（见Object.get_property_list）
static var _dictionary_class_properties_list: Dictionary = {}


# 获取对象的类与脚本名，名称格式见常量：CLASS_AND_SCRIPT_NAME_FORMAT
static func get_object_class_and_script_name(obj: Object) -> String:
	if obj == null:
		return STRING_EMPTY
	var script_name: String = STRING_EMPTY
	var script: Script = obj.get_script()
	if script:
		script_name = script.get_global_name()
	return CLASS_AND_SCRIPT_NAME_FORMAT.format([ obj.get_class(), script_name ])


# 判断对象是否包含指定的属性
static func has_property(obj: Object, property_name: String) -> bool:
	if obj == null:
		return false
	var properties: Array[Dictionary] = get_properties(obj)
	return _has_property(properties, property_name)
	
	
# 判断属性字典数组中是否包含指定的属性
static func _has_property(properties: Array[Dictionary], property_name: String) -> bool:
	if properties == null or properties.is_empty():
		return false;
	return properties.any(func(property) -> bool: return property.name == property_name)


# 获取对象的属性列表，优先从缓存的属性字典中获取，否则获取属性列表并缓存
static func get_properties(obj: Object) -> Array[Dictionary]:
	if not obj:
		return []
	var class_and_script_name: String = get_object_class_and_script_name(obj)
	if _dictionary_class_properties_list.has(class_and_script_name):
		return _dictionary_class_properties_list[class_and_script_name]
	else:
		var properties: Array[Dictionary] = obj.get_property_list()
		_dictionary_class_properties_list[class_and_script_name] = properties
		return properties


# 获取对象所在的全局位置
static func get_global_position(obj: Object) -> Vector2:
	var position: Vector2 = POSITION_FAILED
	if obj and has_property(obj, PROPERTY_NAME_GLOBAL_POSITION):
		position = obj.get(PROPERTY_NAME_GLOBAL_POSITION)
	else:
		position = _get_global_position_from_child(obj)
	return position
	

# 从子节点获取全局位置
# 如果obj是Node2D节点，依次尝试子节点CollisionShape2D、Sprite2D的全局位置
static func _get_global_position_from_child(obj: Object) -> Vector2:
	var node2d: Node2D = obj as Node2D
	if not node2d:
		return POSITION_FAILED
	var children: Array[Node] = node2d.get_children(true)
	if children.is_empty():
		return POSITION_FAILED
	var position: Vector2 = POSITION_FAILED
	for child: Node in children:
		if child is CollisionShape2D:
			var collision_shape_2d: CollisionShape2D = child as CollisionShape2D
			var shape: Shape2D = collision_shape_2d.shape
			if shape:
				position = node2d.to_global(shape.get_rect().position)
				break
	if position == POSITION_FAILED:
		for child: Node in children:
			if child is Sprite2D:
				var sprite_2d: Sprite2D = child as Sprite2D
				position = sprite_2d.global_position
				break
	return position
	
	
# 获取对象所在的全局边界框
static func get_global_rect(obj: Object) -> Rect2:
	var rect: Rect2 = RECT_FAILED
	if obj and obj.has_method(METHOD_NAME_GET_GLOBAL_RECT):
		rect = obj.call(METHOD_NAME_GET_GLOBAL_RECT)
	else:
		rect = _get_global_rect_from_child(obj)
	return rect


# 从子节点获取全局边界框
# 如果obj是Node2D节点，依次尝试子节点CollisionShape2D、Sprite2D的全局边界框
static func _get_global_rect_from_child(obj: Object) -> Rect2:
	var node2d: Node2D = obj as Node2D
	if not node2d:
		return RECT_FAILED
	var children: Array[Node] = node2d.get_children(true)
	if children.is_empty():
		return RECT_FAILED
	var rect: Rect2 = RECT_FAILED
	for child: Node in children:
		if child is CollisionShape2D:
			var collision_shape_2d: CollisionShape2D = child as CollisionShape2D
			var shape: Shape2D = collision_shape_2d.shape
			if shape:
				var local_rect: Rect2 = shape.get_rect()
				rect = Rect2(node2d.to_global(local_rect.position), local_rect.size)
				break
	if rect == RECT_FAILED:
		for child: Node in children:
			if child is Sprite2D:
				var sprite_2d: Sprite2D = child as Sprite2D
				var local_rect: Rect2 = sprite_2d.get_rect()
				rect = Rect2(node2d.to_global(local_rect.position), local_rect.size)
				break
	return rect


# 获取由两个点组成的边界框
static func get_rect_from_two_points(p1: Vector2, p2: Vector2) -> Rect2:
	var _x: float = minf(p1.x, p2.x)
	var _y: float = minf(p1.y, p2.y)
	var _width: float = absf(p1.x - p2.x)
	var _height: float = absf(p1.y - p2.y)
	return Rect2(_x, _y, _width, _height)


# 获取canvas所在视口的边界框
static func get_viewport_rect(canvas: CanvasItem) -> Rect2:
	if canvas:
		return canvas.get_viewport_rect()
	else:
		return RECT_FAILED
	

# 获取指定组中的单位字典
static func get_units_in_groups(scene_tree: SceneTree, groups: Array[String]) -> Dictionary:
	if not scene_tree or not groups or groups.is_empty():
		return {}
	var units: Dictionary = {}
	for group: String in groups:
		var nodes: Array[Node] = scene_tree.get_nodes_in_group(group)
		if not nodes or nodes.is_empty():
			continue
		for node: Node in nodes:
			units[node.get_instance_id()] = node
	return units
#endregion common


#region unit(s) at point
# 获取指定组中，指定位置的单位字典
static func get_units_in_groups_at_point(scene_tree: SceneTree, groups: Array[String], pos: Vector2) -> Dictionary:
	if not scene_tree or not groups or groups.is_empty():
		return {}
	var units_in_groups: Dictionary = get_units_in_groups(scene_tree, groups)
	if not units_in_groups or units_in_groups.is_empty():
		return {}
	var units: Dictionary = {}
	for unit: Node in units_in_groups.values:
		var rect: Rect2 = get_global_rect(unit)
		if rect.has_point(pos):
			units[unit.get_instance_id()] = unit
	return units
	

# 获取单位字典中的顶层单位
static func get_topmost_unit_in_units(units: Dictionary) -> Node:
	if not units or units.is_empty():
		return null
	var topmost: Node = null
	var z_index: int = 0
	for unit: Node in units.values:
		var z: int  = unit.get("z_index")
		if z_index <= z:
			topmost = unit
			z_index = z
	return topmost
	

# 获取指定组中，指定位置的第一个匹配单位
static func get_first_unit_in_groups_at_point(scene_tree: SceneTree, groups: Array[String], pos: Vector2) -> Node:
	if not scene_tree or not groups or groups.is_empty():
		return null
	var units_in_groups: Dictionary = get_units_in_groups(scene_tree, groups)
	if not units_in_groups or units_in_groups.is_empty():
		return null
	for unit: Node in units_in_groups.values:
		var rect: Rect2 = get_global_rect(unit)
		if rect.has_point(pos):
			return unit
	return null
	

# 获取指定组中，指定位置的顶层单位
static func get_topmost_unit_in_groups_at_point(scene_tree: SceneTree, groups: Array[String], pos: Vector2) -> Node:
	var units: Dictionary = get_units_in_groups_at_point(scene_tree, groups, pos)
	return get_topmost_unit_in_units(units)
#endregion unit(s) at point


#region units in range
# 获取指定组中，指定范围的单位字典
static func get_units_in_groups_in_range(scene_tree: SceneTree, groups: Array[String], range: Rect2) -> Dictionary:
	if not scene_tree or not groups or groups.is_empty():
		return {}
	var units_in_groups: Dictionary = get_units_in_groups(scene_tree, groups)
	return get_units_in_range(units_in_groups, range)
	
	
# 获取单位字典中，指定范围的单位字典
static func get_units_in_range(dict: Dictionary, range: Rect2) -> Dictionary:
	if not dict or dict.is_empty():
		return {}
	var units: Dictionary = {}
	for unit: Node in dict.values:
		var rect: Rect2 = get_global_rect(unit)
		if rect.intersects(range):
			units[unit.get_instance_id()] = unit
	return units
#endregion unit(s) in range


#region units in class
# 获取单位字典中，指定类型的单位字典
static func get_units_in_class(dict: Dictionary, class_and_script_name: String) -> Dictionary:
	if not dict or dict.is_empty():
		return {}
	var units: Dictionary = {}
	for unit: Node in dict.values:
		var csn = get_object_class_and_script_name(unit)
		if csn == class_and_script_name:
			units[unit.get_instance_id()] = unit
	return units
#endregion units in class
